Avage täiustatud brauseripõhine videotöötlus. Õppige WebCodecs API abil otse juurde pääsema ja manipuleerima VideoFrame'i tasandite toorandmeid kohandatud efektide ja analüüsi jaoks.
WebCodecs VideoFrame'i tasandite ligipääs: sügav sukeldumine toorvideoandmete manipuleerimisse
Aastaid tundus suure jõudlusega videotöötlus veebibrauseris kauge unistusena. Arendajad olid sageli piiratud <video> elemendi ja 2D Canvas API piirangutega, mis, kuigi võimsad, tekitasid jõudluse kitsaskohti ja piirasid juurdepääsu alusolevatele toorvideoandmetele. WebCodecs API tulek on seda maastikku põhjalikult muutnud, pakkudes madala taseme juurdepääsu brauseri sisseehitatud meediakoodekitele. Üks selle kõige revolutsioonilisemaid funktsioone on võime otse juurde pääseda ja manipuleerida üksikute videokaadrite toorandmeid läbi VideoFrame objekti.
See artikkel on põhjalik juhend arendajatele, kes soovivad liikuda kaugemale lihtsast video taasesitusest. Uurime VideoFrame'i tasandite ligipääsu keerukust, selgitame lahti mõisteid nagu värviruumid ja mälupaigutus ning toome praktilisi näiteid, et anda teile võimalus ehitada järgmise põlvkonna brauserisiseseid videorakendusi, alates reaalajas filtritest kuni keerukate arvutinägemise ülesanneteni.
Eeltingimused
Selle juhendi maksimaalseks ärakasutamiseks peaks teil olema hea arusaam järgmistest teemadest:
- Kaasaegne JavaScript: Sealhulgas asĂĽnkroonne programmeerimine (
async/await, Promises). - Video põhimõisted: Kasulik on tunda termineid nagu kaadrid, eraldusvõime ja koodekid.
- Brauseri API-d: Kogemus API-dega nagu Canvas 2D või WebGL on kasulik, kuid mitte rangelt nõutav.
Videokaadrite, värviruumide ja tasandite mõistmine
Enne kui sukeldume API-sse, peame esmalt looma kindla mentaalse mudeli sellest, millised videokaadri andmed tegelikult välja näevad. Digitaalne video on järjestikune jada liikumatuid pilte ehk kaadreid. Iga kaader on pikslite ruudustik ja igal pikslil on värv. Kuidas seda värvi salvestatakse, määratlevad värviruum ja pikslivorming.
RGBA: veebi emakeel
Enamik veebiarendajaid on tuttavad RGBA värvimudeliga. Iga pikslit esindavad neli komponenti: punane, roheline, sinine ja alfa (läbipaistvus). Andmed salvestatakse tavaliselt mällu põimituna, mis tähendab, et ühe piksli R, G, B ja A väärtused salvestatakse järjestikku:
[R1, G1, B1, A1, R2, G2, B2, A2, ...]
Selles mudelis salvestatakse kogu pilt ühes pidevas mälublokis. Võime seda pidada üheks "andmetasandiks".
YUV: videotihenduse keel
Videokoodekid töötavad aga harva otse RGBA-ga. Nad eelistavad YUV (või täpsemalt Y'CbCr) värviruume. See mudel eraldab pildiinformatsiooni järgmiselt:
- Y (Luma): heledus- ehk hallskaala informatsioon. Inimese silm on kõige tundlikum luma muutustele.
- U (Cb) ja V (Cr): krominants- ehk värvierinevuse informatsioon. Inimese silm on värvi detailide suhtes vähem tundlik kui heleduse detailidele.
See eraldamine on tõhusa tihendamise võti. Vähendades U ja V komponentide eraldusvõimet – tehnikat nimetatakse kroma alasämplimiseks – saame faili suurust oluliselt vähendada minimaalse tajutava kvaliteedikaoga. See viib tasapinnaliste pikslivorminguteni, kus Y, U ja V komponendid salvestatakse eraldi mälublokkidesse ehk "tasanditesse".
Levinud vorming on I420 (YUV 4:2:0 tüüp), kus iga 2x2 pikslibloki kohta on neli Y-sämplit, kuid ainult üks U- ja üks V-sämpel. See tähendab, et U ja V tasanditel on pool Y-tasandi laiusest ja kõrgusest.
Selle erinevuse mõistmine on kriitilise tähtsusega, sest WebCodecs annab teile otsese juurdepääsu just nendele tasanditele, täpselt nii, nagu dekooder neid pakub.
VideoFrame'i objekt: teie värav piksliandmetesse
Selle pusle keskne osa on VideoFrame objekt. See esindab ĂĽhte videokaadrit ja sisaldab lisaks piksliandmetele ka olulisi metaandmeid.
VideoFrame'i peamised omadused
format: String, mis näitab pikslivormingut (nt 'I420', 'NV12', 'RGBA').codedWidth/codedHeight: Kaadri täismõõtmed mällu salvestatuna, sealhulgas igasugune koodeki nõutav polsterdus.displayWidth/displayHeight: Mõõtmed, mida tuleks kasutada kaadri kuvamiseks.timestamp: Kaadri esitluse ajatempel mikrosekundites.duration: Kaadri kestus mikrosekundites.
Maagiline meetod: copyTo()
Peamine meetod toorpiksliandmetele juurdepääsuks on videoFrame.copyTo(destination, options). See asünkroonne meetod kopeerib kaadri tasandiandmed teie pakutud puhvrisse.
destination:ArrayBuffervõi tüübitud massiiv (naguUint8Array), mis on piisavalt suur andmete mahutamiseks.options: Objekt, mis määrab, milliseid tasandeid kopeerida ja nende mälupaigutuse. Kui see on ära jäetud, kopeerib see kõik tasandid ühte pidevasse puhvrisse.
Meetod tagastab Promise'i, mis laheneb PlaneLayout objektide massiiviga, ĂĽks iga kaadri tasandi kohta. Iga PlaneLayout objekt sisaldab kahte olulist teavet:
offset: Baidinihe, kust selle tasandi andmed sihtpuhvris algavad.stride: Baitide arv ühe pikslirea alguse ja järgmise rea alguse vahel sellel tasandil.
Kriitiline mõiste: samm (stride) vs laius
See on üks levinumaid segaduse allikaid arendajatele, kes on madala taseme graafikaprogrammeerimises uued. Te ei saa eeldada, et iga pikslirea andmed on tihedalt üksteise järel pakitud.
- Laius on pikslite arv pildi reas.
- Samm (stride, ka pitch või line step) on baitide arv mälus ühe rea algusest järgmise alguseni.
Sageli on stride suurem kui laius * baidid_piksli_kohta. See on tingitud sellest, et mälu on sageli polsterdatud, et joonduda riistvara piiridega (nt 32- või 64-baidised piirid) protsessori või GPU kiiremaks töötlemiseks. Piksli mäluaadressi arvutamiseks konkreetses reas peate alati kasutama sammu (stride).
Sammu ignoreerimine toob kaasa viltused või moonutatud pildid ja vale andmetele juurdepääsu.
Praktiline näide 1: hallskaala tasandile juurdepääs ja selle kuvamine
Alustame lihtsa, kuid võimsa näitega. Enamik veebis olevast videost on kodeeritud YUV-vormingus, nagu I420. 'Y' tasand on tegelikult täielik pildi hallskaala esitus. Saame eraldada ainult selle tasandi ja renderdada selle lõuendile.
async function displayGrayscale(videoFrame) {
// Eeldame, et videoFrame on YUV-vormingus, nagu 'I420' või 'NV12'.
if (!videoFrame.format.startsWith('I4')) {
console.error('See näide nõuab YUV 4:2:0 tasapinnalist vormingut.');
videoFrame.close();
return;
}
const yPlaneInfo = videoFrame.layout[0]; // Y-tasand on alati esimene.
// Loome puhvri ainult Y-tasandi andmete hoidmiseks.
const yPlaneData = new Uint8Array(yPlaneInfo.stride * videoFrame.codedHeight);
// Kopeerime Y-tasandi meie puhvrisse.
await videoFrame.copyTo(yPlaneData, {
rect: { x: 0, y: 0, width: videoFrame.codedWidth, height: videoFrame.codedHeight },
layout: [yPlaneInfo]
});
// NĂĽĂĽd sisaldab yPlaneData tooreid hallskaala piksleid.
// Peame selle renderdama. Loome lõuendi jaoks RGBA puhvri.
const canvas = document.getElementById('my-canvas');
canvas.width = videoFrame.displayWidth;
canvas.height = videoFrame.displayHeight;
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(canvas.width, canvas.height);
// Käime läbi lõuendi pikslid ja täidame need Y-tasandi andmetega.
for (let y = 0; y < videoFrame.displayHeight; y++) {
for (let x = 0; x < videoFrame.displayWidth; x++) {
// Tähtis: kasutage õige lähteindeksi leidmiseks sammu (stride)!
const yIndex = y * yPlaneInfo.stride + x;
const luma = yPlaneData[yIndex];
// Arvutame sihtkoha indeksi RGBA ImageData puhvris.
const rgbaIndex = (y * canvas.width + x) * 4;
imageData.data[rgbaIndex] = luma; // Punane
imageData.data[rgbaIndex + 1] = luma; // Roheline
imageData.data[rgbaIndex + 2] = luma; // Sinine
imageData.data[rgbaIndex + 3] = 255; // Alfa
}
}
ctx.putImageData(imageData, 0, 0);
// KRIIITILINE: sulgege alati VideoFrame, et selle mälu vabastada.
videoFrame.close();
}
See näide toob esile mitu olulist sammu: õige tasandi paigutuse tuvastamine, sihtpuhvri eraldamine, copyTo kasutamine andmete eraldamiseks ja andmete korrektne läbimine stride'i abil uue pildi konstrueerimiseks.
Praktiline näide 2: kohapealne manipuleerimine (seepiafilter)
Nüüd teostame otsese andmete manipuleerimise. Seepiafilter on klassikaline efekt, mida on lihtne rakendada. Selle näite jaoks on lihtsam töötada RGBA-kaadriga, mille võite saada lõuendilt või WebGL-kontekstist.
async function applySepiaFilter(videoFrame) {
// See näide eeldab, et sisendkaader on 'RGBA' või 'BGRA'.
if (videoFrame.format !== 'RGBA' && videoFrame.format !== 'BGRA') {
console.error('Seepiafiltri näide nõuab RGBA kaadrit.');
videoFrame.close();
return null;
}
// Eraldage puhver piksliandmete hoidmiseks.
const frameDataSize = videoFrame.allocationSize();
const frameData = new Uint8Array(frameDataSize);
await videoFrame.copyTo(frameData);
const layout = videoFrame.layout[0]; // RGBA on ĂĽks tasand
// NĂĽĂĽd manipuleerige andmeid puhvris.
for (let y = 0; y < videoFrame.codedHeight; y++) {
for (let x = 0; x < videoFrame.codedWidth; x++) {
const pixelIndex = y * layout.stride + x * 4; // 4 baiti piksli kohta (R,G,B,A)
const r = frameData[pixelIndex];
const g = frameData[pixelIndex + 1];
const b = frameData[pixelIndex + 2];
const tr = 0.393 * r + 0.769 * g + 0.189 * b;
const tg = 0.349 * r + 0.686 * g + 0.168 * b;
const tb = 0.272 * r + 0.534 * g + 0.131 * b;
frameData[pixelIndex] = Math.min(255, tr);
frameData[pixelIndex + 1] = Math.min(255, tg);
frameData[pixelIndex + 2] = Math.min(255, tb);
// Alfa (frameData[pixelIndex + 3]) jääb muutmata.
}
}
// Looge muudetud andmetega *uus* VideoFrame.
const newFrame = new VideoFrame(frameData, {
format: videoFrame.format,
codedWidth: videoFrame.codedWidth,
codedHeight: videoFrame.codedHeight,
timestamp: videoFrame.timestamp,
duration: videoFrame.duration
});
// Ärge unustage originaalkaadrit sulgeda!
videoFrame.close();
return newFrame;
}
See demonstreerib täielikku lugemise-muutmise-kirjutamise tsüklit: kopeerige andmed välja, käige need läbi sammu abil, rakendage igale pikslile matemaatiline teisendus ja konstrueerige tulemusandmetega uus VideoFrame. Seejärel saab selle uue kaadri renderdada lõuendile, saata VideoEncoder'ile või edastada teisele töötlemisetapile.
Jõudlus on oluline: JavaScript vs WebAssembly (WASM)
Miljonite pikslite läbimine iga kaadri jaoks (1080p kaadril on üle 2 miljoni piksli ehk 8 miljonit andmepunkti RGBA-s) JavaScriptis võib olla aeglane. Kuigi kaasaegsed JS-mootorid on uskumatult kiired, võib see lähenemine kõrge eraldusvõimega video (HD, 4K) reaalajas töötlemisel peamise lõime kergesti üle koormata, põhjustades hakkivat kasutajakogemust.
Siin muutub WebAssembly (WASM) oluliseks tööriistaks. WASM võimaldab teil brauseris käitada C++, Rusti või Go keeles kirjutatud koodi peaaegu natiivse kiirusega. Videotöötluse töövoog muutub järgmiseks:
- JavaScriptis: Kasutage
videoFrame.copyTo(), et saada toorpiksliandmedArrayBuffer'isse. - Edastage WASM-ile: Edastage viide sellele puhvrile oma kompileeritud WASM-moodulisse. See on väga kiire operatsioon, kuna see ei hõlma andmete kopeerimist.
- WASM-is (C++/Rust): Käivitage oma kõrgelt optimeeritud pilditöötlusalgoritmid otse mälupuhvril. See on kordades kiirem kui JavaScripti tsükkel.
- Tagasi JavaScripti: Kui WASM on lõpetanud, naaseb kontroll JavaScripti. Seejärel saate muudetud puhvrit kasutada uue
VideoFrame'i loomiseks.
Iga tõsise, reaalajas videotöötlusrakenduse – näiteks virtuaalsete taustade, objektituvastuse või keerukate filtrite – puhul pole WebAssembly võimendamine mitte lihtsalt valik, vaid paratamatus.
Erinevate pikslivormingute käsitlemine (nt I420, NV12)
Kuigi RGBA on lihtne, saate VideoDecoder'ist enamasti kaadreid tasapinnalistes YUV-vormingutes. Vaatame, kuidas käsitleda täielikult tasapinnalist vormingut nagu I420.
I420-vormingus VideoFrame'il on selle layout massiivis kolm paigutuse kirjeldajat:
layout[0]: Y-tasand (luma). Mõõtmed oncodedWidthxcodedHeight.layout[1]: U-tasand (kroma). Mõõtmed oncodedWidth/2xcodedHeight/2.layout[2]: V-tasand (kroma). Mõõtmed oncodedWidth/2xcodedHeight/2.
Siin on, kuidas kopeerida kõik kolm tasandit ühte puhvrisse:
async function extractI420Planes(videoFrame) {
const totalSize = videoFrame.allocationSize({ format: 'I420' });
const allPlanesData = new Uint8Array(totalSize);
const layouts = await videoFrame.copyTo(allPlanesData);
// layouts on 3 PlaneLayout objekti massiiv
console.log('Y Plane Layout:', layouts[0]); // { offset: 0, stride: ... }
console.log('U Plane Layout:', layouts[1]); // { offset: ..., stride: ... }
console.log('V Plane Layout:', layouts[2]); // { offset: ..., stride: ... }
// Nüüd saate igale tasandile `allPlanesData` puhvris ligi pääseda
// kasutades selle spetsiifilist nihet ja sammu.
const yPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[0].offset,
layouts[0].stride * videoFrame.codedHeight
);
// Pange tähele, et kroma mõõtmed on poole väiksemad!
const uPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[1].offset,
layouts[1].stride * (videoFrame.codedHeight / 2)
);
const vPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[2].offset,
layouts[2].stride * (videoFrame.codedHeight / 2)
);
console.log('Accessed Y plane size:', yPlaneView.byteLength);
console.log('Accessed U plane size:', uPlaneView.byteLength);
videoFrame.close();
}
Teine levinud vorming on NV12, mis on pool-tasapinnaline. Sellel on kaks tasandit: üks Y jaoks ja teine tasand, kus U ja V väärtused on põimitud (nt [U1, V1, U2, V2, ...]). WebCodecs API käsitleb seda läbipaistvalt; NV12-vormingus VideoFrame'il on lihtsalt kaks paigutust oma layout massiivis.
Väljakutsed ja parimad praktikad
Sellel madalal tasemel töötamine on võimas, kuid sellega kaasnevad ka kohustused.
Mäluhaldus on esmatähtis
VideoFrame hoiab kinni märkimisväärse hulga mälu, mida sageli hallatakse väljaspool JavaScripti prügikoguja kuhja. Kui te seda mälu selgesõnaliselt ei vabasta, põhjustate mälulekke, mis võib brauseri vahekaardi kokku jooksutada.
Alati, alati kutsuge välja videoFrame.close(), kui olete kaadriga lõpetanud.
AsĂĽnkroonne olemus
Kogu andmetele juurdepääs on asünkroonne. Teie rakenduse arhitektuur peab korrektselt käsitlema Promise'ide ja async/await voogu, et vältida võidujooksu tingimusi ja tagada sujuv töötlemistoru.
Brauseri ĂĽhilduvus
WebCodecs on kaasaegne API. Kuigi see on toetatud kõigis suuremates brauserites, kontrollige alati selle saadavust ja olge teadlik mis tahes tootjaspetsiifilistest rakendusdetailidest või piirangutest. Kasutage enne API kasutamist funktsioonide tuvastamist.
Kokkuvõte: uus ajastu veebivideos
Võimalus otse juurde pääseda ja manipuleerida VideoFrame'i toortasandi andmeid WebCodecs API kaudu on paradigma nihe veebipõhiste meediarakenduste jaoks. See eemaldab <video> elemendi musta kasti ja annab arendajatele peene kontrolli, mis oli varem reserveeritud natiivsetele rakendustele.
Mõistes video mälupaigutuse aluseid – tasandid, samm ja värvivormingud – ning võimendades WebAssembly jõudlust nõudvate operatsioonide jaoks, saate nüüd ehitada uskumatult keerukaid videotöötlustööriistu otse brauseris. Alates reaalajas värvide korrigeerimisest ja kohandatud visuaalsetest efektidest kuni kliendipoolse masinõppe ja videoanalüüsini on võimalused laiad. Suure jõudlusega, madala taseme video ajastu veebis on tõeliselt alanud.